package nl.altenpts.net.kad;

import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import nl.altenpts.net.broker.IncomingMessage;
import nl.altenpts.net.broker.ResponseHandler;

public class KBucket implements Serializable {

	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	
	
	public KBucket(Id root, int prefixlength) {
		this.root = root;
		this.prefixlength = prefixlength;
		childbucket = null;
		lastnodefind = 0;
	}
	
	public void split() {
		
		assert(nodes != null);
		
		childbucket = new KBucket(root, prefixlength + 1);
		
		synchronized(nodes) {
			Iterator<Node> iterator = nodes.iterator();
			Node n;
			while(iterator.hasNext()) {
				n = iterator.next();
				if(n.getId().matchesPrefix(root, prefixlength + 1)) {
					iterator.remove();
					childbucket.updateNode(n, null);
				}			
			}
		}
	}
	
	
	public boolean isSplitted() {
		return childbucket != null;
	}
	
	
	public void findNodes(List<Node> result, Id target, int n, Comparator<Node> distcomp) {

		if(target.getBit(prefixlength) == root.getBit(prefixlength) && isSplitted()) {
			childbucket.findNodes(result, target, n, distcomp);
			if(result.size() < n) {
				addLocalNodes(result, target, n, distcomp);
			}
		} else {
			addLocalNodes(result, target, n, distcomp);
			if(result.size() < n && isSplitted()) {
				childbucket.findNodes(result, target, n, distcomp);
			}
		}
	}
	
	
	public void updateNode(Node node, KBroker broker) {
		
		boolean matchchild = node.getId().matchesPrefix(root, prefixlength + 1);
		synchronized(nodes) {
			if(nodes.contains(node)) {
				nodes.remove(node);
			}

			if(!isSplitted() && (nodes.size() >= Kademlia.k)) {
				split();
			}

			if(isSplitted() && matchchild) {
				childbucket.updateNode(node, broker);
			} else {
				if(nodes.size() == Kademlia.k) {
					/* after serialization/deserialization, newnodes may be null
					 * because it is transient field. 
					 */
					if(newnodes == null) {
						newnodes = new LinkedList<Node>();
					}
					newnodes.addLast(node);
					if(newnodes.size() == 1) {
						checkNewNodes(broker);
					}
				} else {
					nodes.add(node);
				}		
			}
		}
	}
	
	public void didNodeFind(Id node, int prefixmatch) {
		if(isSplitted() && prefixmatch > prefixlength) {
			childbucket.didNodeFind(node, prefixmatch);
		} else {
			lastnodefind = System.currentTimeMillis();
		}
	}
	
	public void refresh(Kademlia kad) {		
		
		if(Kademlia.freshbucket + lastnodefind < System.currentTimeMillis()) {
			kad.findNodes(Id.getRandomId(root, prefixlength, isSplitted()), null);
		}
		
		if(isSplitted()) {
			childbucket.refresh(kad);
		}
	}

	private void addLocalNodes(List<Node> result, Id target, int n, Comparator<Node> distcomp) {
		synchronized(nodes) {
			Iterator<Node> iterator = nodes.iterator();
			int index;
			while(iterator.hasNext()) {
				Node next = iterator.next();

				index = Collections.binarySearch(result, next, distcomp);				
				index = -index - 1;

				if(index >= 0 && index < n) {
					result.add(index, next);
					if(result.size() > n) {
						result.remove(n);
					}
				}				
			}
		}
	}
	
	public void checkNewNodes(KBroker broker) {

		synchronized(nodes) {
			Node n = nodes.getFirst();
			if(!newnodes.isEmpty()) {
				broker.PingRequest(n, new NewNodeResponseHandler(n, broker));
			}
		}
	}
	
	public void writeTable() {
		
		System.out.print("bucket " + prefixlength + ": " );
		for(Iterator<Node> it = nodes.iterator(); it.hasNext(); ) {
			System.out.print(it.next().getAddress().getPort() + ", ");

		}
		System.out.println();
		
		if(childbucket != null) {
			childbucket.writeTable();
		}
	}
	
	
	class NewNodeResponseHandler implements ResponseHandler {
		
		public NewNodeResponseHandler(Node oldnode, KBroker broker) {
			this.oldnode = oldnode;
			this.broker = broker;
		}
		
		public void onResponse(IncomingMessage response) {
			synchronized(nodes) {
				newnodes.removeFirst();
				if(!newnodes.isEmpty()) {
					checkNewNodes(broker);
				}
			}
		}

		public void onTimeout() {
			synchronized(nodes) {
				nodes.remove(oldnode);
				nodes.addLast(newnodes.removeFirst());
				if(!newnodes.isEmpty()) {
					checkNewNodes(broker);
				}
			}			
		}
		
		private Node oldnode;
		private KBroker broker;
	}
	

	private LinkedList<Node> nodes = new LinkedList<Node>();
	private KBucket childbucket;
	private transient LinkedList<Node> newnodes = new LinkedList<Node>();
	private long lastnodefind;
	
	private int prefixlength;
	private Id root;
}
